#include "world.h"
#include <basecode\gl\glew.h>
#include <basecode\gl\glfw.h>
//#include <basecode\gl\soil.h>
#include <basecode\perlin.h>
#include <cmath>
#include <algorithm>
#include "shader.h"
#include "Game.h"
#include "glm/gtc/type_ptr.hpp"
Perlin *perlinLoFrequency;
Perlin *perlinHiFrequency;


#define INDEX(x,y) ( ((y)*SIZ)+(x) )


struct vec2_t
{
	float x,y;
};

World::World(Game* _game)
{
	game = _game;
	worldShader = new Shader();
	worldShader->addStage(std::pair<GLenum, const GLchar*>(GL_VERTEX_SHADER, "../data/shaders/world.vert"));
	worldShader->addStage(std::pair<GLenum, const GLchar*>(GL_FRAGMENT_SHADER, "../data/shaders/world.frag"));
	worldShader->install();

	simShader = new Shader();
	simShader->addStage(std::pair<GLenum, const GLchar*>(GL_VERTEX_SHADER, "../data/shaders/sim.vert"));
	simShader->addStage(std::pair<GLenum, const GLchar*>(GL_FRAGMENT_SHADER, "../data/shaders/sim.frag"));
	simShader->install();


	scale = 1.0f;
	target = 0;

	glGenTextures(2, &tex[0]);
	if( tex == 0 )
	{
		throw "could not alloc tex handle.. weird...";
	}

	perlinLoFrequency = new Perlin(2,6,1,4); 
	genmap();
	initFBO();
	
}

void World::genmap()
{
	float noised[2];
	for(int i=0; i<SIZ; i++)
	{
		for(int j=0; j<SIZ; j++)
		{
			float x = i / (float)SIZ;
			float y = j / (float)SIZ;
			noised[0] = x;
			noised[1] = y;
			int clamped = 0;
			float noiseValue = perlinLoFrequency->Get( x,y );
			if( noiseValue <= 0.0f ) noiseValue = 0.0f; // cutoff

			if( noiseValue > 0.0 ) clamped = 1;
			map[INDEX(i,j)] = clamped;
		}
	}

	regenRegion(SIZ/2, SIZ/2, SIZ);
}


void World::regenRegion( int x, int y, int boxSize )
{
	int xmin = x - boxSize/2;
	int xmax = x + boxSize/2;
	int ymin = y - boxSize/2;
	int ymax = y + boxSize/2;
	xmin = std::max<int>(1, xmin);
	ymin = std::max<int>(1, ymin);
	xmax = std::min<int>(SIZ-1, xmax);
	ymax = std::min<int>(SIZ-1, ymax);

	for(int i=xmin; i<xmax; i++)
	{
		for(int j=ymin; j<ymax; j++)
		{

			int threshold = map[INDEX(i,j)];

			int colorIdx = INDEX(i,j)*4;

			if( threshold == 0.0 ) // Black if low noise amp
			{
				colormap[colorIdx+0] = colormap[colorIdx+1] = colormap[colorIdx+2] = colormap[colorIdx+3] = 0.0f;
			}else{
				float x = i / (float)SIZ;
				float y = j / (float)SIZ;
				float noiseValue = std::max<float>(0, perlinLoFrequency->Get(x,y) );
				colormap[colorIdx+0] = 128.0f/255.0f+ (noiseValue)*200.0f/255.0f;
				colormap[colorIdx+1] = 60.0f/255.0f + (noiseValue)*128.0f/255.0f;
				colormap[colorIdx+2] = 20.0f/255.0f + (noiseValue)*40.0f/255.0f;
				colormap[colorIdx+3] = 1.0f;
			}
		}
	}

	findEdges(xmin, ymin, xmax, ymax);
	calcNormals(xmin, ymin, xmax, ymax);
	smoothmap(xmin, ymin, xmax, ymax);

	updateTexture(xmin, ymin, xmax, ymax);
}


void World::updateTexture(int xmin, int ymin, int xmax, int ymax)
{
	glBindTexture(GL_TEXTURE_2D, tex[target]);
	glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST );// when texture area is small    
	glTexParameterf( GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST ); // when texture area is large. Try to swap GL_NEAREST with GL_LINEAR
	// if wrap is true, the texture wraps over at the edges (repeat)  false, the texture ends at the edges (clamp)

	int wrapMode;
	if (0)  
		wrapMode = GL_REPEAT;
	else 
		wrapMode = GL_CLAMP;

	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_S, wrapMode ); 
	glTexParameteri( GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, wrapMode );
	glTexEnvf( GL_TEXTURE_ENV, GL_TEXTURE_ENV_MODE, GL_MODULATE ); // select modulate to mix texture with color for shading
	glTexParameteri(GL_TEXTURE_2D, GL_GENERATE_MIPMAP, GL_FALSE );
	
	static int firstUpload = 1;
	if ( !firstUpload ) 
		return;
	if( firstUpload ) 
	{
		
		glTexImage2D(GL_TEXTURE_2D, 0, GL_RGB, SIZ, SIZ, 0, GL_RGBA, GL_FLOAT, &colormap[0] );
		firstUpload = 0;
	}
	else
	{
		//glPixelStorei(GL_UNPACK_ALIGNMENT, 4); // Default is 4 (RGBA). use 1 if RGB
		int dirtyW = xmax-xmin;
		int dirtyH = ymax-ymin;

		for(int i=0; i<dirtyW; i++)
		{
			for(int j=0; j<dirtyH; j++)
			{
				int dirtyIdx = (j*dirtyW+i)*4;
				//int dirtyIdx = (i*dirtyW+j)*4;

				int ix = xmin + i;
				int iy = ymin + j;
				int colorIdx = INDEX(ix,iy)*4;

				dirtyRect[dirtyIdx+0] = colormap[colorIdx+0];
				dirtyRect[dirtyIdx+1] = colormap[colorIdx+1];
				dirtyRect[dirtyIdx+2] = colormap[colorIdx+2];
				dirtyRect[dirtyIdx+3] = colormap[colorIdx+3];
			}
		}

		glTexSubImage2D(GL_TEXTURE_2D, 0, xmin, ymin, dirtyW, dirtyH, GL_RGBA, GL_FLOAT, &dirtyRect[0] );
	}
}

void World::initFBO()
{
	glGenFramebuffersEXT(1, &fbo[0]); // create FBO 

	glBindFramebuffer(GL_FRAMEBUFFER, fbo[0]); 
	glBindTexture(GL_TEXTURE_2D, tex[0]); 
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[0], 0); // attach texture to FBO

	glGenFramebuffersEXT(1, &fbo[1]); // create FBO 
	glBindFramebuffer(GL_FRAMEBUFFER, fbo[1]); 
	glBindTexture(GL_TEXTURE_2D, tex[1]); 
	glTexParameterf(GL_TEXTURE_2D, GL_TEXTURE_WRAP_T, GL_CLAMP);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MAG_FILTER, GL_NEAREST);
	glTexParameteri(GL_TEXTURE_2D, GL_TEXTURE_MIN_FILTER, GL_NEAREST);
	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SIZ, SIZ, 0, GL_RGBA, GL_UNSIGNED_BYTE, 0x0);
	glFramebufferTexture2D(GL_FRAMEBUFFER, GL_COLOR_ATTACHMENT0, GL_TEXTURE_2D, tex[1], 0); // attach texture to FBO
	
	glBindFramebuffer(GL_FRAMEBUFFER, 0);
}


void World::draw()
{
	static char texBuf[SIZ*SIZ*4];
	//updateTexture(0,0,SIZ,SIZ);

	glEnable(GL_TEXTURE_2D);
	glColor3f(1,1,1);
	
	float sx = SIZ * scale;
	float sy = SIZ * scale;

	glActiveTexture(GL_TEXTURE0);

	glBindFramebuffer(GL_FRAMEBUFFER, fbo[getNotTarget()]); 
	{
		simShader->begin();
		{
			glBindTexture(GL_TEXTURE_2D, tex[target]);
			glUniform1i(simShader->getUniLoc("textureTarget"), 0);

			glBegin (GL_QUADS);
			glTexCoord2f (0.0f, 0.0f); glVertex2f (0.0f, 0.0f);
			glTexCoord2f (1.0f, 0.0f); glVertex2f (sx, 0.0f);
			glTexCoord2f (1.0f, 1.0f); glVertex2f (sx, sy);
			glTexCoord2f (0.0f, 1.0f); glVertex2f (0.0f, sy);
			glEnd ();
		} simShader->end();
	} glBindFramebuffer(GL_FRAMEBUFFER, 0);
	
	worldShader->begin();
	{
		glBindTexture(GL_TEXTURE_2D, tex[getNotTarget()]);
		glUniform1i(worldShader->getUniLoc("worldTexture"), 0);

		glUniform2fv( worldShader->getUniLoc("playerPos"), 1, glm::value_ptr(game->getPlayerPos()));

		glBegin (GL_QUADS);
		glTexCoord2f (0.0f, 0.0f); glVertex2f (0.0f, 0.0f);
		glTexCoord2f (1.0f, 0.0f); glVertex2f (sx, 0.0f);
		glTexCoord2f (1.0f, 1.0f); glVertex2f (sx, sy);
		glTexCoord2f (0.0f, 1.0f); glVertex2f (0.0f, sy);
		glEnd();
	} worldShader->end();

	swapTarget();

	glDisable(GL_TEXTURE_2D);

//	glReadPixels(0,0,SIZ, SIZ, GL_RGBA, GL_FLOAT, colormap);

//	glTexImage2D(GL_TEXTURE_2D, 0, GL_RGBA, SIZ, SIZ, 0, GL_RGBA, GL_FLOAT, &colormap[0] );
}

void World::findEdges(int xmin, int ymin, int xmax, int ymax)
{
	for(int i=xmin; i<xmax; i++)
	{
		for(int j=ymin; j<ymax; j++)
		{
			int U = map[INDEX(i,j-1)];
			int D = map[INDEX(i,j+1)];
			int L = map[INDEX(i-1,j)];
			int R = map[INDEX(i+1,j)];

			if( map[INDEX(i,j)] == 0 && (U==1 || D==1 || L==1 || R==1) )
			{
				edgemap[INDEX(i,j)] = 1;
			}
			else
			{
				edgemap[INDEX(i,j)] = 0;
			}
		} 
	}
}

void World::calcNormals(int xmin, int ymin, int xmax, int ymax)
{  
	for(int i=xmin; i<xmax; i++)
	{
		for(int j=ymin; j<ymax; j++)
		{
			if( edgemap[INDEX(i,j)] == 1 )
			{
				struct vec2_t v;
				v.x = 0; v.y = 0;
				int hasU = 0;
				int hasD = 0;
				int hasL = 0;
				int hasR = 0;
				int neigh = 0; // holds count of how which edge neighbours this pixel has

				if( map[INDEX(i,j-1)] == 1)
				{
					hasU = 1;
					neigh += 1;
				}
				if( map[INDEX(i,j+1)] == 1)
				{
					hasD = 1;
					neigh += 1;
				}
				if( map[INDEX(i-1,j)] == 1)
				{
					hasL = 1;
					neigh += 1;
				}
				if( map[INDEX(i+1,j)] == 1)
				{
					hasR = 1;
					neigh += 1;
				}

				if( neigh == 0 ){ v.x = 0; v.y = 0; }
				else if( neigh == 1 ) // has 1 neighbour, -the neighbor vector
				{
					if( hasL ){  v.x = 1;  v.y = 0; }   
					if( hasR ){  v.x = -1; v.y = 0; }   
					if( hasU ){  v.x = 0; v.y = 1; }      
					if( hasD ){  v.x = 0; v.y = -1; }  
				} 
				else if( neigh == 2 ) // angular. down left or right, up left or right.
				{
					if( hasD && hasU ){ v.x = 0; v.y = 0; }
					else if( hasL && hasR ){ v.x = 0; v.y = 0; }

					else if( hasD && hasR ){ v.x = -1; v.y = -1; }
					else if( hasD && hasL ){ v.x = 1; v.y = -1; }
					else if( hasU && hasR ){ v.x = -1; v.y = 1; }
					else if( hasU && hasL ){ v.x = 1; v.y = 1; }
					else
					{ // UP & DOWN or L & R
						int avgx =( hasL + -hasR );
						int avgy =( hasD + -hasU );           
						v.x = -(float)avgx; v.y = -(float)avgy; 
					}
				}
				else if( neigh == 3 ) // left+right && up||down
				{
					// v = the non-neighbor vector
					v.x = 0; v.y = 0;
					if( hasL )  v.x += 1;     
					if( hasR )  v.x -= 1;  
					if( hasU )  v.y += 1;     
					if( hasD )  v.y -= 1;    
				}

				if(  neigh > 0 )
				{
					float rlen  = (float) 1.0 / sqrt(v.x*v.x + v.y*v.y);
					v.x *= rlen;
					v.y *= rlen;  
				}
				normalMap[INDEX(i,j)] = atan2(v.y,v.x); // convert cartesian (x,y) to angle
			}
		}
	}
}  




void World::smoothmap(int xmin, int ymin, int xmax, int ymax)
{  
	for(int i=xmin; i<xmax; i++)
	{
		for(int j=ymin; j<ymax; j++)
		{
			if(edgemap[INDEX(i,j)] == 1)
			{
				double current = normalMap[INDEX(i,j)];
				double U = normalMap[INDEX(i,j-1)];
				double D = normalMap[INDEX(i,j+1)];
				double L = normalMap[INDEX(i-1,j)];
				double R = normalMap[INDEX(i+1,j)];


				double UL = normalMap[INDEX(i-1,j-1)];
				double UR = normalMap[INDEX(i+1,j-1)];

				double DL = normalMap[INDEX(i-1,j+1)];
				double DR = normalMap[INDEX(i+1,j+1)];

				int count = 1;
				double avgx=0; double avgy=0;
				avgx += cos(current); avgy += sin(current);

				int limit = 1;
				if( edgemap[INDEX(i,j-1)] == limit ){ avgx += cos(U); avgy += sin(U); count++;}
				if( edgemap[INDEX(i,j+1)] == limit ){ avgx += cos(D); avgy += sin(D); count++;}
				if( edgemap[INDEX(i-1,j)] == limit ){ avgx += cos(L); avgy += sin(L); count++;}
				if( edgemap[INDEX(i+1,j)] == limit ){ avgx += cos(R); avgy += sin(R); count++;}

				if( edgemap[INDEX(i-1,j-1)] == limit ){ avgx += cos(UL); avgy += sin(UL); count++;}
				if( edgemap[INDEX(i+1,j-1)] == limit ){ avgx += cos(UR); avgy += sin(UR); count++;}
				if( edgemap[INDEX(i-1,j+1)] == limit ){ avgx += cos(DL); avgy += sin(DL); count++;}
				if( edgemap[INDEX(i+1,j+1)] == limit ){ avgx += cos(DR); avgy += sin(DR); count++;}

				avgx /= count; avgy /= count;

				smoothedNormals[INDEX(i,j)] = atan2(avgy, avgx); // convert back to angle.
			}
		}  
	}
}


void World::drawNormals()
{
	glColor3f(0,1,0);  
	glBegin(GL_LINES);
	for(int j=0; j<SIZ; j++)
	{
		for(int i=0; i<SIZ; i++)
		{  
			if( edgemap[INDEX(i,j)] )
			{
				float ang = (float)smoothedNormals[INDEX(i,j)];
				float x = cos(ang); float y = sin(ang);
					
					glColor3f(1,0,0);
					glVertex2f(i * scale, j *scale ); 
					glColor3f(0,1,0);
					glVertex2f(i* scale + x* scale * 4 , j* scale + y* scale * 4);
					
			}
		}
	}
	glEnd();
	
}


void World::paintbrush(int x, int y, int c, int r)
{
	r-=1;
	int i=0; int j=0;
	for(i=0; i<2*r; i++)
	{
		double len = int( sqrt( cos(0.5*3.14159*(i-r)/r)) * r * 2 );
		for(j=0; j<len; j++)
		{
			double xp = x+j-len/2;
			double yp = y-r+i;
			if(xp > 0 && xp < (SIZ) && yp > 0 && yp < (SIZ))
			{
				map[INDEX( (int)xp, (int)yp )] = c;
			}
		}
	}
}

void World::getClosestPointAndNormal( glm::vec2 &testPoint, float ballradi, glm::vec2 &closestPoint, glm::vec2 &normal, bool &isValid  )
{
	float mind = 1e32f;
	int closestx = -1; int closesty = -1;
	int xmin = int(testPoint[0] - ballradi*4);
	int ymin = int(testPoint[1] - ballradi*4);
	int xmax = int(testPoint[0] + ballradi*4);
	int ymax = int(testPoint[1] + ballradi*4);

	xmin = std::max<int>(1, xmin);
	ymin = std::max<int>(1, ymin);
	xmax = std::min<int>(SIZ-1, xmax);
	ymax = std::min<int>(SIZ-1, ymax);

	for(int i=xmin; i<xmax; i++)
		for(int j=ymin; j<ymax; j++)
		{
			if( edgemap[INDEX(i,j)] ) //==1
			{
				float dx = (testPoint[0] - i);
				float dy = (testPoint[1] - j);
				float d = dx*dx + dy*dy;

				if( d < mind )
				{
					mind = d;
					closestx = i;
					closesty = j;
				}
			}
		}

	// Get normal from pixel position
	float angle = (float)smoothedNormals[INDEX(closestx,closesty)];
	normal = glm::vec2(cosf(angle), sinf(angle) );
	if( closestx != -1 && closesty != -1 )
	{
		isValid = true;
		closestPoint = glm::vec2( closestx, closesty );
	}else{
		isValid = false;
	}
}
